# Copyright (c) 2010-2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# NOTE: swift_conn
# You'll see swift_conn passed around a few places in this file. This is the
# source httplib connection of whatever it is attached to.
#   It is used when early termination of reading from the connection should
# happen, such as when a range request is satisfied but there's still more the
# source connection would like to send. To prevent having to read all the data
# that could be left, the source connection can be .close() and then reads
# commence to empty out any buffers.
#   These shenanigans are to ensure all related objects can be garbage
# collected. We've seen objects hang around forever otherwise.

from __future__ import print_function
import mimetypes
import os
import time
from ConfigParser import ConfigParser
import uuid

from eventlet import Timeout

from swift.common.ring import Ring
from swift.common.utils import cache_from_env, get_logger, \
    get_remote_client, split_path, config_true_value
from swift.common.constraints import check_utf8
from swift.proxy.controllers import AccountController, ObjectController, \
    ContainerController, Controller
from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPForbidden, \
    HTTPMethodNotAllowed, HTTPNotFound, HTTPPreconditionFailed, \
    HTTPRequestEntityTooLarge, HTTPRequestTimeout, HTTPServerError, \
    HTTPServiceUnavailable, HTTPClientDisconnect, status_map, Request, Response

try:
    # simplejson is popular and pretty good                                      
    from simplejson import loads as json_loads
    from simplejson import dumps as json_dumps
except ImportError:
    # 2.6 will have a json module in the stdlib                                  
    from json import loads as json_loads
    from json import dumps as json_dumps


class Application(object):
    """WSGI application for the proxy server."""

    def __init__(self, conf, memcache=None, logger=None, account_ring=None,
                 container_ring=None, object_ring=None):
        if conf is None:
            conf = {}
        if logger is None:
            self.logger = get_logger(conf, log_route='proxy-server')
        else:
            self.logger = logger

        swift_dir = conf.get('swift_dir', '/etc/swift')
        self.node_timeout = int(conf.get('node_timeout', 10))
        self.conn_timeout = float(conf.get('conn_timeout', 0.5))
        self.client_timeout = int(conf.get('client_timeout', 60))
        self.put_queue_depth = int(conf.get('put_queue_depth', 10))
        self.object_chunk_size = int(conf.get('object_chunk_size', 65536))
        self.client_chunk_size = int(conf.get('client_chunk_size', 65536))
        self.error_suppression_interval = \
            int(conf.get('error_suppression_interval', 60))
        self.error_suppression_limit = \
            int(conf.get('error_suppression_limit', 10))
        self.recheck_container_existence = \
            int(conf.get('recheck_container_existence', 60))
        self.recheck_account_existence = \
            int(conf.get('recheck_account_existence', 60))
        self.allow_account_management = \
            config_true_value(conf.get('allow_account_management', 'no'))
        self.object_post_as_copy = \
            config_true_value(conf.get('object_post_as_copy', 'true'))
        self.resellers_conf = ConfigParser()
        self.resellers_conf.read(os.path.join(swift_dir, 'resellers.conf'))
        self.object_ring = object_ring or Ring(swift_dir, ring_name='object')
        self.container_ring = container_ring or Ring(swift_dir,
                                                     ring_name='container')
        self.account_ring = account_ring or Ring(swift_dir,
                                                 ring_name='account')
        self.memcache = memcache
        mimetypes.init(mimetypes.knownfiles +
                       [os.path.join(swift_dir, 'mime.types')])
        self.account_autocreate = \
            config_true_value(conf.get('account_autocreate', 'no'))
        self.expiring_objects_account = \
            (conf.get('auto_create_account_prefix') or '.') + \
            'expiring_objects'
        self.expiring_objects_container_divisor = \
            int(conf.get('expiring_objects_container_divisor') or 86400)
        self.max_containers_per_account = \
            int(conf.get('max_containers_per_account') or 0)
        self.max_containers_whitelist = [
            a.strip()
            for a in conf.get('max_containers_whitelist', '').split(',')
            if a.strip()]
        self.deny_host_headers = [
            host.strip() for host in
            conf.get('deny_host_headers', '').split(',') if host.strip()]
        self.rate_limit_after_segment = \
            int(conf.get('rate_limit_after_segment', 10))
        self.rate_limit_segments_per_sec = \
            int(conf.get('rate_limit_segments_per_sec', 1))
        self.log_handoffs = config_true_value(conf.get('log_handoffs', 'true'))
        self.cors_allow_origin = [
            a.strip()
            for a in conf.get('cors_allow_origin', '').split(',')
            if a.strip()]

    def get_controller(self, path):
        """
        Get the controller to handle a request.

        :param path: path from request
        :returns: tuple of (controller class, path dictionary)

        :raises: ValueError (thrown by split_path) if given invalid path
        """
        version, account, container, obj = split_path(path, 1, 4, True)
        d = dict(version=version,
                 account_name=account,
                 container_name=container,
                 object_name=obj)
        if obj and container and account:
            return ObjectController, d
        elif container and account:
            return ContainerController, d
        elif account and not container and not obj:
            return AccountController, d
        return None, d

    def __call__(self, env, start_response):
        """
        WSGI entry point.
        Wraps env in swob.Request object and passes it down.

        :param env: WSGI environment dictionary
        :param start_response: WSGI callable
        """
        try:
            if self.memcache is None:
                self.memcache = cache_from_env(env)
            req = self.update_request(Request(env))
            return self.handle_request(req)(env, start_response)
        except UnicodeError:
            err = HTTPPreconditionFailed(
                request=req, body='Invalid UTF8 or contains NULL')
            return err(env, start_response)
        except (Exception, Timeout):
            start_response('500 Server Error',
                           [('Content-Type', 'text/plain')])
            return ['Internal server error.\n']

    def update_request(self, req):
        if 'x-storage-token' in req.headers and \
                'x-auth-token' not in req.headers:
            req.headers['x-auth-token'] = req.headers['x-storage-token']
        return req


#  This function filters the resp body when doing an Account Get (list) for
#  a VO member.  The VO member gets to see only the container names whose
#  read or write ACL they are on.

    def vo_filter_body (self, resp, pparts, tenant_user_name):
        resp_bytes_used = eval(resp.headers['x-account-bytes-used'])
        resp_container_count = eval(resp.headers['x-account-container-count'])
        resp_object_count = eval(resp.headers['x-account-object-count'])

        new_body = []
        for container in eval(resp.body):
            pparts['container_name'] = container['name']
            contcont = ContainerController( self, **pparts )
            cont_info = contcont.container_info( pparts['account_name'], pparts['container_name'] )

            permissions = ""
            if cont_info['read_acl'] != None:
                if  tenant_user_name in cont_info['read_acl']:
                    permissions = "R"
            if cont_info['write_acl'] != None:
                if  tenant_user_name in cont_info['write_acl']:
                    permissions += "W"
            if  len(permissions) != 0:
                container['permissions'] = permissions
                new_body.append( container )
            else:
                resp_container_count -= 1
                resp_object_count -= container['count']
                resp_bytes_used -= container['bytes']

#  Even though resp.body has getter/setter functions that seem to handle strings just fine,
#  there is some difference between regular "strings" and json "strings".  Changing the
#  resp.body string in any way causes json_loads(body) in get_account() in the client to fail.
#  This includes using str(new_body) to set resp.body -- even when the setter function seems
#  to be completely happy.
#  Using:
#       new_body_str = str(new_body)
#       resp.body = ''.join(new_body)
#  and replacing json_loads(body) with eval(body) on the client side seemed to get things to
#  work, but retaining the json_loads() on the client side and using json_dumps() on the
#  server side also seems to work, and would avoid any other potential problems caused by regular
#  "strings" being different than json "strings".

        jsonbody = json_dumps(new_body)
        resp.body = jsonbody

        resp.headers['x-account-bytes-used'] = str(resp_bytes_used)
        resp.headers['x-account-container-count'] = str(resp_container_count)
        resp.headers['x-account-object-count'] = str(resp_object_count)

        vo_log_file = open("/opt/stack/vo_test_log.txt", "a", 0)
        print("resp_container_count", resp_container_count, file=vo_log_file)
        print("resp_object_count", resp_object_count, file=vo_log_file)
        print("resp_bytes_used", resp_bytes_used, file=vo_log_file)
        print("json body len: ", len(jsonbody), file=vo_log_file)
        print("json body", jsonbody, file=vo_log_file)
        vo_log_file.close()
        return resp


    def handle_request(self, req):
        """
        Entry point for proxy server.
        Should return a WSGI-style callable (such as swob.Response).

        :param req: swob.Request object
        """
        try:
	    vo_log_file = open("/opt/stack/vo_test_log.txt", "a", 0)
	    print("===== Proxy handler starting =====", file=vo_log_file)
#	    print("req: ", vars(req), file=vo_log_file )
#	    print("req path: ", req.path, file=vo_log_file )
#	    print("req path info: ", req.path_info, file=vo_log_file )
	    vo_log_file.close()

            self.logger.set_statsd_prefix('proxy-server')
            if req.content_length and req.content_length < 0:
                self.logger.increment('errors')
                return HTTPBadRequest(request=req,
                                      body='Invalid Content-Length')

            try:
                if not check_utf8(req.path_info):
                    self.logger.increment('errors')
                    return HTTPPreconditionFailed(
                        request=req, body='Invalid UTF8 or contains NULL')
            except UnicodeError:
                self.logger.increment('errors')
                return HTTPPreconditionFailed(
                    request=req, body='Invalid UTF8 or contains NULL')

            try:
                controller, path_parts = self.get_controller(req.path)
                p = req.path_info
                if isinstance(p, unicode):
                    p = p.encode('utf-8')
            except ValueError:
                self.logger.increment('errors')
                return HTTPNotFound(request=req)

#	    vo_log_file = open("/opt/stack/vo_test_log.txt", "a", 0)
#	    print("controller: ", controller, file=vo_log_file )
#	    print("path parts: ", path_parts, file=vo_log_file )
#	    vo_log_file.close()

            if not controller:
                self.logger.increment('errors')
                return HTTPPreconditionFailed(request=req, body='Bad URL')
            if self.deny_host_headers and \
                    req.host.split(':')[0] in self.deny_host_headers:
                return HTTPForbidden(request=req, body='Invalid host header')

            self.logger.set_statsd_prefix('proxy-server.' +
                                          controller.server_type.lower())

#	    vo_log_file = open("/opt/stack/vo_test_log.txt", "a", 0)
#	    print("===== Defining controller =====", file=vo_log_file)
#	    vo_log_file.close()

            controller = controller(self, **path_parts)

            if 'swift.trans_id' not in req.environ:
                # if this wasn't set by an earlier middleware, set it now
                trans_id = 'tx' + uuid.uuid4().hex
                req.environ['swift.trans_id'] = trans_id
                self.logger.txn_id = trans_id
            req.headers['x-trans-id'] = req.environ['swift.trans_id']
            controller.trans_id = req.environ['swift.trans_id']
            self.logger.client_ip = get_remote_client(req)
            try:
                handler = getattr(controller, req.method)
                getattr(handler, 'publicly_accessible')
            except AttributeError:
                allowed_methods = getattr(controller, 'allowed_methods', set())
                return HTTPMethodNotAllowed(
                    request=req, headers={'Allow': ', '.join(allowed_methods)})
            if path_parts['version']:
                req.path_info_pop()

# If we're doing a VO list (this is a VO member), don't check authorization here
# check for VO.project and role != admin (or role == Member)

            env_tenant_name = req.environ['HTTP_X_TENANT_NAME']
            env_user_role = req.environ['HTTP_X_ROLE']
            env_req_method = req.environ['REQUEST_METHOD']

            isVOmember = env_tenant_name.startswith( "VO." ) and (env_user_role == "Member")
            isAccountGETop = isinstance(controller, AccountController) and (env_req_method == "GET")
            isVOAccountGETop = isVOmember and isAccountGETop

#            vo_log_file = open("/opt/stack/vo_test_log.txt", "a", 0)
#            print("isacctcontrl: ", isinstance(controller, AccountController), file=vo_log_file)
#            print("ismethod: ", (env_req_method == "GET"), file=vo_log_file)
#            print("isVOmember: ", isVOmember, file=vo_log_file)
#            print("isAccountGETop: ", isAccountGETop, file=vo_log_file)
#            print("isVOAccountGETop: ", isVOAccountGETop, file=vo_log_file)
#            print("env: ", env_tenant_name, env_user_role, env_req_method, file=vo_log_file)
#            vo_log_file.close()
  
            # If this is a "list" by a VO member, then skip the authorization checks here
            # but filter the container names in the reply body based on who's on the RW ACLs
            # before sending the response
            if not isVOAccountGETop:
                if 'swift.authorize' in req.environ:
                    # We call authorize before the handler, always. If authorized,
                    # we remove the swift.authorize hook so isn't ever called
                    # again. If not authorized, we return the denial unless the
                    # controller's method indicates it'd like to gather more
                    # information and try again later.
                    resp = req.environ['swift.authorize'](req)
                    if not resp:
                        # No resp means authorized, no delayed recheck required.
                        del req.environ['swift.authorize']
                    else:
                        # Response indicates denial, but we might delay the denial
                        # and recheck later. If not delayed, return the error now.
                        if not getattr(handler, 'delay_denial', None):
                            return resp

            # Save off original request method (GET, POST, etc.) in case it
            # gets mutated during handling.  This way logging can display the
            # method the client actually sent.
            req.environ['swift.orig_req_method'] = req.method

            resp = handler(req)

            # If a VO member is doing a list, then filter out the container names in the body
            # of the response where the VO member does not appear on the ACLs
            if  isVOAccountGETop:
                tenant_user_name = env_tenant_name + ":" + req.environ['HTTP_X_USER_NAME']
                resp = self.vo_filter_body( resp, path_parts, tenant_user_name )

#	    print("handler rv: ", vars(resp), file=vo_log_file )
#	    print("===== Proxy handler finishing =====\n", file=vo_log_file )
#	    vo_log_file.close()
            return resp
        except (Exception, Timeout):
            self.logger.exception(_('ERROR Unhandled exception in request'))
            return HTTPServerError(request=req)


def app_factory(global_conf, **local_conf):
    """paste.deploy app factory for creating WSGI proxy apps."""
    conf = global_conf.copy()
    conf.update(local_conf)
    return Application(conf)
